﻿using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using CSharpExt.IO.Compression;

namespace CSharpExt.IO
{
    /*
     * I file ome:
     * 
     *      name.ext
     *      name[1].ext
     *      name[2].ext
     * 
     * se compresso
     * 
     *      name.ext
     *      name[1].zip / name[1].gz
     *      name[2].zip / name[2].gz
     */
    public class FileChunk
    {
        #region Options Class
        // ==================================================================
        // Internal Class
        // ==================================================================

        public class Options
        {
            public Options()
            {
                Pattern = "{0}[{1}]{2}";
                ChunkSize = 8 * 1024 * 1024;
                NumOfChunks = 4;
            }


            /// <summary>
            /// Dimensione, in byte, del chunk.
            /// 0 se illimitato
            /// </summary>
            public int ChunkSize { get; set; }

            /// <summary>
            ///  Numero massimo di chunk.
            /// 0 se illimitato
            /// </summary>
            public int NumOfChunks { get; set; }

            /// <summary>
            /// Se i chunk devono essere compressi (usa GZip)
            /// </summary>
            public bool Compress { get; set; }

            /// <summary>
            /// Se la compressione in scrittura deve avvenire in un thread separato
            /// (preso dal pool)
            /// </summary>
            public bool ThreadedCompress { get; set; }

            /// <summary>
            /// Se il controllo sulla dimensione del chunk deve essere fatto
            /// dopo ogni 'Write'. Altrimenti il controllo lo fa su ogni 'Flush'
            /// (default)
            /// </summary>
            public bool CheckOnWrite
            {
                get { return !CheckOnFlush; }
                set { CheckOnFlush = !value; }
            }

            /// <summary>
            /// Se il controllo sulla dimensione del chunk deve essere fatto
            /// sul 'Flush'. Altrimenti il controllo lo fa su ogni 'Write'
            /// </summary>
            public bool CheckOnFlush { get; set; }

            /// <summary>
            /// Se deve usare GZipStream
            /// Per default usa Zip Inflate
            /// </summary>
            public bool UseGZip { get; set; }

            /// <summary>
            /// Pattern usato per generare il nome dei file.
            /// I parametri passati al pattern sono:
            /// 
            /// {0}: il nome del file
            /// {1}: l'indice
            /// {2}: l'estensione (con il '.') (che puo' essere ".gz" se compresso)
            /// 
            /// Per default e':  "{0}[{1}]{2}
            /// 
            /// </summary>
            public String Pattern { get; set; }

            /// <summary>
            /// Append          [write] continua con il file corrente
            /// Create          [write] rinomina il file corrente e inizia con un file nuovo
            /// CreateNew       [write] elimina tutti i file esistenti
            /// Open            [read]  il file DEVE esistere
            /// OpenOrCreate    [NO]    Non supportato
            /// Truncate        [write] come Create
            /// </summary>
            public FileMode Mode { get; set; }

            /// <summary>
            /// Read
            /// ReadWrite       [NO] Non supportato
            /// Write
            /// </summary>
            public FileAccess Access { get; set; }

            /// <summary>
            /// Delete          [??]
            /// Inheritable     [??]
            /// None
            /// Read
            /// ReadWrite       [NO] Non supportato
            /// Write
            /// </summary>
            public FileShare Share { get; set; }
        }

        private abstract class ChunkStream : Stream
        {
            #region Protected Data
            // ==================================================================
            // Protected Data
            // ==================================================================

            protected String _path;

            protected String _directory;
            protected String _name;
            protected String _extension;

            protected Options _options;
            protected Stream _stream;

            #endregion

            #region Constructor
            // ==================================================================
            // Constructor
            // ==================================================================

            internal ChunkStream(String path, Options options)
            {
                _path = path;
                _options = options;

                splitPath();
            }

            #endregion

            #region Operations
            // ==================================================================
            // Operations
            // ==================================================================

            internal abstract Stream Open();

            #endregion

            #region Stream Overrides
            // ==================================================================
            // Stream Overrides
            // ==================================================================

            public override void Flush()
            {
                _stream.Flush();
            }

            public override void Close()
            {
                if (_stream != null)
                    _stream.Close();
                base.Close();
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                throw new InvalidOperationException();
            }

            public override void SetLength(long value)
            {
                throw new InvalidOperationException();
            }

            public override int Read(byte[] buffer, int offset, int count)
            {
                throw new InvalidOperationException();
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                throw new InvalidOperationException();
            }

            public override bool CanRead
            {
                get { return _stream.CanRead; }
            }

            public override bool CanSeek
            {
                get { return _stream.CanSeek; }
            }

            public override bool CanWrite
            {
                get { return _stream.CanWrite; }
            }

            public override long Length
            {
                get { throw new InvalidOperationException(); }
            }

            public override long Position
            {
                get { return _stream.Position; }
                set { _stream.Position = value; }
            }

            #endregion

            #region Implementation
            // ==================================================================
            // Implementation
            // ==================================================================

            private void splitPath()
            {
                _directory = Path.GetDirectoryName(_path);

                String filename = Path.GetFileName(_path);
                int dot = filename.LastIndexOf('.');

                if (dot > 0)
                    _name = filename.Substring(0, dot);
                else
                    _name = filename;

                _extension = Path.GetExtension(_path);
            }

            protected String chunkPath(int id)
            {
                if (id == 0)
                    return _path;
                else if (_options.Compress)
                    return Path.Combine(_directory, String.Format(_options.Pattern, _name, id, ".gz"));
                else
                    return Path.Combine(_directory, String.Format(_options.Pattern, _name, id, _extension));
            }

            protected int findNextId()
            {
                for (int id = 0; ; ++id)
                {
                    String cpath = chunkPath(id);
                    if (!File.Exists(cpath))
                        return id;
                }
            }

            #endregion
        }

        private class ChunkInputStream : ChunkStream
        {
            #region Private Data
            // ==================================================================
            // Private Data
            // ==================================================================

            private int _currentId;

            #endregion

            #region Constructor
            // ==================================================================
            // Constructor
            // ==================================================================

            internal ChunkInputStream(String path, Options options)
                : base(path, options)
            {

            }

            #endregion

            #region Stream Overrides
            // ==================================================================
            // Stream Overrides
            // ==================================================================

            internal override Stream Open()
            {
                _currentId = findNextId();
                if (_currentId == 0)
                    throw new FileNotFoundException(String.Format("Could not find file '{0}'.", _path));
                openChunk();
                return this;
            }

            public override int Read(byte[] buffer, int offset, int count)
            {
            retry:
                if (_stream == null)
                    return 0;

                int readed = _stream.Read(buffer, offset, count);
                if (readed == 0)
                {
                    closeChunk();
                    openChunk();
                    goto retry;
                }
                return readed;
            }

            #endregion

            #region Implementation
            // ==================================================================
            // Implementation
            // ==================================================================

            private void openChunk()
            {
                if (_currentId == 0)
                    return;

                --_currentId;

                String cpath = chunkPath(_currentId);
                if (_currentId > 0 && _options.Compress)
                {
                    Stream fstream = File.Open(cpath, _options.Mode, _options.Access, _options.Share);
                    _stream = _options.UseGZip
                                  ? (Stream)new GZipStream(fstream, CompressionMode.Decompress)
                                  : (Stream)new ZlibStream(fstream, CompressionMode.Decompress);
                }
                else
                {
                    _stream = File.Open(cpath, _options.Mode, _options.Access, _options.Share);
                }
            }

            private void closeChunk()
            {
                _stream.Close();
                _stream = null;
            }

            #endregion
        }

        private class ChunkOutputStream : ChunkStream
        {
            #region Private Data
            // ==================================================================
            // Private Data
            // ==================================================================

            private int _writed;

            #endregion

            #region Constructor
            // ==================================================================
            // Constructor
            // ==================================================================

            internal ChunkOutputStream(String path, Options options)
                : base(path, options)
            {

            }

            #endregion

            #region Stream Overrides
            // ==================================================================
            // Stream Overrides
            // ==================================================================

            internal override Stream Open()
            {
                switch (_options.Mode)
                {
                    case FileMode.CreateNew:
                        clearChunks();
                        break;

                    case FileMode.Truncate:
                        renameChunks();
                        break;

                    case FileMode.Append:
                        break;

                    case FileMode.Create:
                        renameChunks();
                        break;
                }

                openChunk(true);

                return this;
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                _stream.Write(buffer, offset, count);
                _writed += count;

                if (_options.CheckOnWrite && _writed >= _options.ChunkSize)
                {
                    closeChunk();
                    renameChunks();
                    openChunk(false);
                }
            }

            public override void Flush()
            {
                base.Flush();
                if (_options.CheckOnFlush && _writed >= _options.ChunkSize)
                {
                    closeChunk();
                    renameChunks();
                    openChunk(false);
                }
            }

            #endregion

            #region Implementation
            // ==================================================================
            // Implementation
            // ==================================================================

            private void clearChunks()
            {
                int nextId = findNextId();
                for (int id = 0; id < nextId; ++id)
                    deleteChunk(id);
            }

            private void renameChunks()
            {
                int nextId = findNextId();
                for (int id = nextId - 1; id >= 0; --id)
                {
                    renameChunk(id, id + 1);
                }
                if (_options.NumOfChunks > 0)
                    deleteChunk(_options.NumOfChunks);
            }

            private void openChunk(bool firstTime)
            {
                if (firstTime)
                    _stream = File.Open(_path, _options.Mode, _options.Access, _options.Share);
                else
                    _stream = File.Open(_path, FileMode.CreateNew, _options.Access, _options.Share);
            }

            private void closeChunk()
            {
                _stream.Close();
                _writed = 0;
            }

            private void deleteChunk(int id)
            {
                String cpath = chunkPath(id);

                File.Delete(cpath);
            }

            private void renameChunk(int idSource, int idDest)
            {
                String cpaths = chunkPath(idSource);
                String cpathd = chunkPath(idDest);
                File.Delete(cpathd);

                if (idSource != 0 || !_options.Compress)
                {
                    File.Move(cpaths, cpathd);
                }
                else if (_options.ThreadedCompress)
                {
                    cpathd = cpaths + ".temp";
                    File.Move(cpaths, cpathd);
                    new Thread(threadedCompress).Start();
                }
                else
                {
                    compressChunk(cpaths, cpathd);
                }
            }

            private void threadedCompress()
            {
                String cpaths = chunkPath(0) + ".temp";
                String cpathd = chunkPath(1);

                compressChunk(cpaths, cpathd);
            }

            private void compressChunk(String cpaths, String cpathd)
            {
                using (Stream sinp = File.OpenRead(cpaths))
                using (Stream czip = _options.UseGZip
                    ? (Stream)new GZipStream(File.OpenWrite(cpathd), CompressionMode.Compress)
                    : (Stream)new ZlibStream(File.OpenWrite(cpathd), CompressionMode.Compress))
                {
                    byte[] buffer = new byte[4096];

                    for (int readed = sinp.Read(buffer, 0, 4096);
                        readed > 0;
                        readed = sinp.Read(buffer, 0, 4096))
                    {
                        czip.Write(buffer, 0, readed);
                    }
                }

                File.Delete(cpaths);
            }

            #endregion
        }

        #endregion

        #region Factory Methods
        // ==================================================================
        // Factory Methods
        // ==================================================================

        public static Stream Open(String path, Options options)
        {
            if (options.Access == FileAccess.Read)
                return new ChunkInputStream(path, options).Open();
            if (options.Access == FileAccess.Write)
                return new ChunkOutputStream(path, options).Open();
            throw new InvalidOperationException("FileChunk.Open::FileAccess.ReadWrite");
        }

        public static Stream OpenWrite(String path)
        {
            return Open(path, new Options
            {
                ChunkSize = 8 * 1024 * 1024,
                NumOfChunks = 4,

                Access = FileAccess.Write,
                Mode = FileMode.Create,
                Share = FileShare.Read,

                CheckOnFlush = true,
                Compress = false,
                ThreadedCompress = false
            });
        }

        public static Stream OpenRead(String path)
        {
            return Open(path, new Options
            {
                Access = FileAccess.Read,
                Mode = FileMode.Open,
                Share = FileShare.Read,

                Compress = false,
            });
        }

        #endregion
    }
}
